/**
 * commander 命令行工具：http://blog.fens.me/nodejs-commander/
 * 用法不止一种，其关键能力如下：
 * 自记录代码；
 * 自动生成帮助；
 * 合并短参数（“ABC”==“-A-B-C”）；
 * 默认选项；
 * 强制选项​；
 * 命令解析；
 * 提示符；
 * 
 * 用法可以是链式调用，可以一次设置多个命令，也可以单个设置单个解析。
 */
const { program, Option } = require("../../../node_modules/webpack-cli/node_modules/commander");
const { logInfo, logStep, logError } = require('../../util/logInfo');
class WebpackCli {

    constructor() {

        this.webpack = require('../../webpack/lib/index');
        this.program = program;
        this.program.name('webpack');
        this.program.configureOutput({
            writeErr: (str) => {
                console.log(str);
            },
            outputError: (str) => {
                console.log(str);
            }
        })
        logStep('webpack cli constructor')
        return this;
    }

    getBuiltInOptions() {
        if (this.builtInOptionsCache) {
            return this.builtInOptionsCache;
        }

        const mminimumHelpFlags = [
            "config",
            "node",
            "watch",
            "devtool",
            "entry",
            "output-path"
        ]

        const builtInFlags = [
            {
                name: 'config',
                alias: 'c',
                configs: [
                    {
                        type: "string"
                    }
                ],
                multiple: true,
                description: "Provide path to a webpack configuration file e.g.. ./webpack.config.js"
            },
            {
                name: "entry",
                configs: [
                    {
                        type: "string",
                    },
                ],
                multiple: true,
                description: "The entry point(s) of your application e.g. ./src/main.js.",
            },
            {
                name: "output-path",
                alias: "o",
                configs: [
                    {
                        type: "string",
                    },
                ],
                description: "Output location of the file generated by webpack e.g. ./dist/.",
            }, {
                name: "devtool",
                configs: [
                    {
                        type: "string",
                    },
                    {
                        type: "enum",
                        values: [false],
                    },
                ],
                negative: true,
                alias: "d",
                description: "Determine source maps to use.",
                negatedDescription: "Do not generate source maps.",
            }, {
                name: "watch",
                configs: [
                    {
                        type: "boolean",
                    },
                ],
                negative: true,
                alias: "w",
                description: "Watch for files changes.",
                negatedDescription: "Do not watch for file changes.",
            },
        ]

        logInfo('这里是cli插件机制的核心，通过 this.webpack.cli.getArguments()  方法获取全部的webapck插件，并注入到command 中，是对 cli 的命令扩展机制。')
        const coreFlags = this.webpack.cli ?
            Object.entries(this.webpack.cli.getArguments()).map(([flag, meta]) => {
                const inBuiltIn = builtInFlags.find(builtInFlag => builtInFlags.name === flag);

                if (inBuiltIn) {
                    return {
                        ...meta,
                        name: flag,
                        group: 'croe',
                        ...inBuiltIn,
                        configs: meta.configs || []
                    }
                }
                return { ...meta, name: flag, group: 'croe' };
            })
            : [];
        const options = []
            .concat(
                builtInFlags.filter(
                    builtinFlag =>
                        !coreFlags.find(coreflag => builtinFlag.name === coreflag.name)
                )
            )
            .concat(coreFlags)
            .map((option) => {
                option.helpLevel === mminimumHelpFlags.includes(option.name) ? 'minimum' : 'verbose';
                return option;
            })

        this.builtInOptionsCache = options;
        return options;
    }

    makeCommand(commandOptions, options, action) {
        logInfo('如果是已经包含在program 中的命令，就不需要再创建了。')
        const command = this.program.command(commandOptions.name, {
            noHelp: commandOptions.noHelp,
            hidden: commandOptions.hidden,
            isDefault: commandOptions.isDefault,
        });
        if (commandOptions.description) {
            command.description(commandOptions.description, commandOptions.argsDescription);
        }
        if (commandOptions.usage) {
            command.usage(commandOptions.usage);
        }
        if (Array.isArray(commandOptions.alias)) {
            command.aliases(commandOptions.alias);
        } else {
            command.alias(command.alias);
        }
        if (commandOptions.pkg) {
            command.pkg = commandOptions.pkg;
        } else {
            command.pkg = "webpack-cli";
        }

        const { forHelp } = this.program;
        let allDependenciesInstalled = true;
        if (commandOptions.dependencies && commandOptions.dependencies.length > 0) {
            logInfo('安装这个命令需要的依赖。')
        }
        if (options) {
            logInfo('填充命令中的option参数')
            if (typeof options === 'function') {
                options = options();
            }
            options.forEach(optionForCommand => {
                this.makeOption(command, optionForCommand);
            })
        }

        command.action(action);
        return command;
    }

    makeOption(command, option) {
        // logInfo('填充命令中的option参数:' + JSON.stringify(option))
        let mainOption;
        let negativeOption;

        if (option.configs) {
            let needNegativeOption = false;
            const mainOptionType = new Set();

            option.configs.forEach((config) => {
                // Possible value: "enum" | "string" | "path" | "number" | "boolean" | "RegExp" | "reset"
                switch (config.type) {
                    case "reset":
                        mainOptionType.add(Boolean);
                        break;
                    case "boolean":
                        if (!needNegativeOption) {
                            needNegativeOption = true;
                        }

                        mainOptionType.add(Boolean);
                        break;
                    case "number":
                        mainOptionType.add(Number);
                        break;
                    case "string":
                    case "path":
                    case "RegExp":
                        mainOptionType.add(String);
                        break;
                    case "enum": {
                        let hasFalseEnum = false;

                        const enumTypes = config.values.map((value) => {
                            switch (typeof value) {
                                case "string":
                                    mainOptionType.add(String);
                                    break;
                                case "number":
                                    mainOptionType.add(Number);
                                    break;
                                case "boolean":
                                    if (!hasFalseEnum && value === false) {
                                        hasFalseEnum = true;
                                        break;
                                    }

                                    mainOptionType.add(Boolean);
                                    break;
                            }
                        });

                        if (!needNegativeOption) {
                            needNegativeOption = hasFalseEnum;
                        }

                        return enumTypes;
                    }
                }
            });

            mainOption = {
                flags: option.alias ? `-${option.alias}, --${option.name}` : `--${option.name}`,
                description: option.description || "",
                type: mainOptionType,
                multiple: option.multiple,
                defaultValue: option.defaultValue,
            };

            if (needNegativeOption) {
                negativeOption = {
                    flags: `--no-${option.name}`,
                    description: option.negatedDescription
                        ? option.negatedDescription
                        : `Negative '${option.name}' option.`,
                };
            }
        }

        if (mainOption.type.size > 1 && mainOption.type.has(Boolean)) {
            mainOption.flags = `${mainOption.flags} [value${mainOption.multiple ? "..." : ""}]`;
        } else if (mainOption.type.size > 0 && !mainOption.type.has(Boolean)) {
            mainOption.flags = `${mainOption.flags} <value${mainOption.multiple ? "..." : ""}>`;
        }

        if (mainOption.type.size === 1) {
            if (mainOption.type.has(Number)) {
                let skipDefault = true;

                const optionForCommand = new Option(mainOption.flags, mainOption.description)
                    .argParser((value, prev = []) => {
                        if (mainOption.defaultValue && mainOption.multiple && skipDefault) {
                            prev = [];
                            skipDefault = false;
                        }

                        return mainOption.multiple
                            ? [].concat(prev).concat(Number(value))
                            : Number(value);
                    })
                    .default(mainOption.defaultValue);

                optionForCommand.helpLevel = option.helpLevel;

                command.addOption(optionForCommand);
            } else if (mainOption.type.has(String)) {
                let skipDefault = true;

                const optionForCommand = new Option(mainOption.flags, mainOption.description)
                    .argParser((value, prev = []) => {
                        if (mainOption.defaultValue && mainOption.multiple && skipDefault) {
                            prev = [];
                            skipDefault = false;
                        }

                        return mainOption.multiple ? [].concat(prev).concat(value) : value;
                    })
                    .default(mainOption.defaultValue);

                optionForCommand.helpLevel = option.helpLevel;

                command.addOption(optionForCommand);
            } else if (mainOption.type.has(Boolean)) {
                const optionForCommand = new Option(
                    mainOption.flags,
                    mainOption.description,
                ).default(mainOption.defaultValue);

                optionForCommand.helpLevel = option.helpLevel;

                command.addOption(optionForCommand);
            } else {
                const optionForCommand = new Option(mainOption.flags, mainOption.description)
                    .argParser(Array.from(mainOption.type)[0])
                    .default(mainOption.defaultValue);

                optionForCommand.helpLevel = option.helpLevel;

                command.addOption(optionForCommand);
            }
        } else if (mainOption.type.size > 1) {
            let skipDefault = true;

            const optionForCommand = new Option(
                mainOption.flags,
                mainOption.description,
                mainOption.defaultValue,
            )
                .argParser((value, prev = []) => {
                    if (mainOption.defaultValue && mainOption.multiple && skipDefault) {
                        prev = [];
                        skipDefault = false;
                    }

                    if (mainOption.type.has(Number)) {
                        const numberValue = Number(value);

                        if (!isNaN(numberValue)) {
                            return mainOption.multiple
                                ? [].concat(prev).concat(numberValue)
                                : numberValue;
                        }
                    }

                    if (mainOption.type.has(String)) {
                        return mainOption.multiple ? [].concat(prev).concat(value) : value;
                    }

                    return value;
                })
                .default(mainOption.defaultValue);

            optionForCommand.helpLevel = option.helpLevel;

            command.addOption(optionForCommand);
        } else if (mainOption.type.size === 0 && negativeOption) {
            const optionForCommand = new Option(mainOption.flags, mainOption.description);

            // Hide stub option
            optionForCommand.hideHelp();
            optionForCommand.helpLevel = option.helpLevel;

            command.addOption(optionForCommand);
        }

        if (negativeOption) {
            const optionForCommand = new Option(negativeOption.flags, negativeOption.description);

            optionForCommand.helpLevel = option.helpLevel;

            command.addOption(optionForCommand);
        }
    }

    async buildCommand(options, isWatchCommand) {
        logStep('这一步是在执行命令了，核心是 this.createCompiler 方法');
        const callback = (error, stats) => {
            logInfo('这里是创建 compiler 的回调处理，主要处理：错误退出程序，或者输出一些日志。');
        };
        const env = isWatchCommand || options.watch ? { WEBPACK_WATCH: true, ...options.env } : { WEBPACK_BUILD: true, WEBPACK_BUNDLE: true, ...options.env };
        options.argv = { ...options, env };
        if (isWatchCommand) {
            options.watch = true;
        }

        logStep('创建编译器')
        const compiler = await this.createCompiler(options, callback);
        if (!compiler) {
            return;
        }

        // const isWatch = (compiler) => {
        //     return compiler.compilers
        //         ? compiler.compilers.some(complier => complier.options.watch)
        //         : complier.options.watch
        // }

    }

    async createCompiler(options, callback) {
        logStep('组装配置，创建complier编译器。到这里为止，cli的作用也就结束了，下一步就是调用webpack了')
        let config = await this.resolveConfig(options);
        config = await this.applyOptions(config, options);
        config = await this.applyCLIPlugin(config, options);

        let complier;
        try {
            complier = this.webpack(
                config.options,
                callback
            )
        } catch (error) {
            logError('creat complier error')
            console.log(error);
        }
        logStep(`webpack-cli 的主要功能是对 命令的组装和管理，
        将命令分为 build、watch 和 非 build、watch 两类。
        对于build、watch 封装了基础的参数校验。
        通过调试发现通过 webpack 注入了 550 多个命令。
        这里只有对 help 和 version 的命令实现，其他命令的实现都是插件实现的。`)
        return complier;
    }

    async resolveConfig() {
        logStep('读取配置文件，加载配置文件中的配置');
        return {};
    }

    async applyOptions() {
        logStep('合并命令行和配置文件中的配置');
        return {};
    }

    async applyCLIPlugin(config, cliOptions) {
        logStep('合并插件中的配置，这里通过 CLIPlugin 类方法来重新格式化了插件的配置。');
        return {};
    }

    async run(args, parseOptions) {
        logStep('webpack cli run start')
        const buildCommandOptions = {
            name: 'build [entries]',
            alias: ['bundle', 'b'],
            description: 'Run webpack (default command, can be ommited).',
            usage: "[entries...] [options]"
        }

        const watchCommandOptions = {
            name: "watch [entries...]",
            alias: "w",
            description: "Run webpack and watch for files changes.",
            usage: "[entries...] [options]",
        };
        const versionCommandOptions = {
            name: "version [commands...]",
            alias: "v",
            description:
                "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.",
        };
        const helpCommandOptions = {
            name: "help [command] [option]",
            alias: "h",
            description: "Display help for commands and options.",
        };

        // Built-in external commands
        const externalBuiltInCommandsInfo = [
            {
                name: "serve [entries...]",
                alias: ["server", "s"],
                pkg: "@webpack-cli/serve",
            },
            {
                name: "info",
                alias: "i",
                pkg: "@webpack-cli/info",
            },
            {
                name: "init",
                alias: ["create", "new", "c", "n"],
                pkg: "@webpack-cli/generators",
            },
            {
                name: "loader",
                alias: "l",
                pkg: "@webpack-cli/generators",
            },
            {
                name: "plugin",
                alias: "p",
                pkg: "@webpack-cli/generators",
            },
            {
                name: "migrate",
                alias: "m",
                pkg: "@webpack-cli/migrate",
            },
            {
                name: "configtest [config-path]",
                alias: "t",
                pkg: "@webpack-cli/configtest",
            },
        ];

        const knownCommands = [
            buildCommandOptions,
            watchCommandOptions,
            versionCommandOptions,
            helpCommandOptions,
            ...externalBuiltInCommandsInfo,
        ];

        const getCommandName = (name) => name.split(' ')[0];
        const isCommand = (input, commandOptions) => {
            const longName = getCommandName(commandOptions.name);
            if (input === longName) {
                return true;
            }

            if (commandOptions.alias) {
                if (Array.isArray(commandOptions.alias)) {
                    return commandOptions.alias.includes(input);
                } else {
                    return commandOptions.alias === input;
                }
            }
        };
        const isKnownCommand = (name) => {
            return knownCommands.find((command) => {
                if (getCommandName(command.name) === name) {
                    return true;
                }
                if (Array.isArray(command.alias)) {
                    return command.alias.includes(name);
                } else {
                    return command.alias === name;
                }
            })
        };

        const loadCommandByName = async (commandName, allowToInstall = false) => {
            logStep('加载命令')
            const isBuildCommandUsed = isCommand(commandName, buildCommandOptions);
            const isWatchCommandUse = isCommand(commandName, watchCommandOptions);
            if (isBuildCommandUsed || isWatchCommandUse) {
                logInfo('build 或 watch 命令')
                const options = this.getBuiltInOptions();
                await this.makeCommand(
                    isBuildCommandUsed ? buildCommandOptions : watchCommandOptions,
                    isWatchCommandUse ? option.filter(option => option.name !== 'watch') : options,
                    async (entries, options) => {
                        if (entries && entries.length > 0) {
                            options.entry = [...entries, ...entries(options.entry || [])]
                        }
                        await this.buildCommand(options, isWatchCommandUse);
                    },
                )
            } else if (isCommand(commandName, helpCommandOptions)) {

            } else if (isCommand(commandName, versionCommandOptions)) {

            } else {

            }
        }


        this.program.exitOverride(async (error) => {
            console.log(error);
        })

        const cli = this;
        logInfo('是否开启控制台彩色输出')
        this.program.option('--color', 'Enable colors on console.');
        this.program.on('option:color', function () {
            logInfo('program 的一种用法： program.option + program.on');
            console.log('need color');
        })

        this.program.option(
            "-v, --version",
            "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.",
        );
        this.program.on('option:version', function () {
            console.log('---- version');
        })

        const outputHelp = async (options, isVerbose, isHelpCommandSyntax, program) => {
            logStep('拼接并输出 help 内容')
        };

        const outputVersion = () => {
            logStep('输出 webpack-dev-serve version');
        }

        this.program.helpOption(false);
        this.program.addHelpCommand(false);
        this.program.option("-h, --help [verbose]", "Display help for commands and options.");

        this.program.allowUnknownOption(true);
        this.program.usage('[options]');

        this.program.action(async (options, program) => {
            console.log(options);
            const { operands, unknown } = this.program.parseOptions(program.args);
            const defaultCommandToRun = getCommandName(buildCommandOptions.name);
            const hasOperand = typeof operands[0] !== "undefined";
            const operand = hasOperand ? operands[0] : defaultCommandToRun;
            const isHelpOption = typeof options.help !== 'undefined';
            const isHelpCommandSyntax = isCommand(operand, helpCommandOptions);
            if (isHelpOption || isHelpCommandSyntax) {
                //TODO: help 相关
                await outputHelp();
            }

            const isVersionOption = typeof options.version !== "undefined";
            const isVersionCommanddSyntax = isCommand(operand, versionCommandOptions);
            if (isVersionOption || isVersionCommanddSyntax) {
                outputVersion()
            }

            logStep('获取要执行的命令，并判断是否是已知命令')
            let commandToRun = operand;
            let commandOperands = operands.slice(1);

            if (isKnownCommand(commandToRun)) {
                logStep('是已知命令，则加载改命令。');
                await loadCommandByName(commandToRun, true);
            } else {
                logInfo('判断是否是制定的文件地址');
                const isEntrySyntax = fs.existsSync(operand);
                if (isEntrySyntax) {
                    logStep(' 这种持场景即使用默认build 命令，参数只输入入口文件。')
                    commandToRun = defaultCommandToRun;
                    commandOperands = operands;

                    await loadCommandByName(commandToRun);
                } else {
                    logStep('未知的命令。尝试寻找相似的命令，并抛出提示。');
                    logInfo('Did you mean xxx ?')
                    logInfo('这里的关键在于找出相似的命令: 利用 levenshtein 插件找出相似的。')
                }
            }

            await this.program.parseAsync([commandToRun, ...commandOperands, ...unknown], {
                from: 'user'
            })

        })

        await this.program.parseAsync(args, parseOptions);


        console.log(args);
    }
}

module.exports = WebpackCli;